Advanced animation
Basics
Way 1
Consider to use to get the highest frame-rate and smooth animations
with TransitionType set to None
if the calculation time does not exceed the 1/60
of the second. Otherwise consider to use "Linear"
interpolation option and a small amount of TransitionDuration around 10-100 depending on how long it takes to update the data.
If you animation looks sloppy, you can always cheat asking Javascript to interpolate between data portions over the time. Use TransitionType and TransitionDuration
For example
balls = RandomReal[{-1,1}, {100,3}];
vels = RandomReal[{-1,1}, {100,3}];
Graphics3D[{
Table[With[{i = i},
{
RGBColor[RandomReal[{0,1}, 3]],
Sphere[balls[[i]] // Offload, 0.03]
}
], {i, Length[balls]}],
AnimationFrameListener[balls // Offload, "Event"->"frame"]
}]
evaluate the cell above. It will create a canvas with randomly scattered balls
At the start of the browser's frame, an event "frame"
is triggered to request an update of data from the Kernel. However, after this, you'll need to "recharge" an AnimationFrameListener
, otherwise it will not trigger the event again. This can be automated to occur whenever a change in the balls
symbol is detected.
This process ensures the following benefits:
- Synchronization of animation with the browser's engine (eliminating flickering).
- Ability to skip frames if recalculations take longer than one frame of your browser, adapting to your computing power.
Here is our update function
EventHandler["frame", Function[Null,
vels = Table[
If[Norm[balls[[i]]] < 0.01, -1, 1] vels[[i]] - 0.08 balls[[i]]
, {i, Length[balls]}];
balls = Table[balls[[i]] + 0.08 vels[[i]], {i, Length[balls]}];
]];
To start an animation - reevaluate cell 1 or use this "kickstarter"
EventFire["frame", Null]
Way 2
Consider to use SetInterval
for simple or resource intensive animation. Set TransitionDuration and TransitionType to a proper value to interpolate the values.
Usually if your SetInterval
is let's say 100 ms
, then TransitionDuration should be around 100 ms
as well to get the smoothest animation.
If you animation looks sloppy, you can always cheat asking Javascript to interpolate between data portions over the time. Use TransitionType and TransitionDuration
For example
ParametricAnimator[equation_, variable_:t, range_:{0, Infinity, 0.1}] := LeakyModule[{time = range[[1]], task, scale = 1, array = {}, scaledArray={}, cell = ResultCell[]},
(* sample the equation each frame and rescale if needed *)
animate := Block[{variable = time},
With[{e = {Sin[t], Cos[t]} equation},
scale = If[Norm[e scale] > 1.4, scale 0.95, scale 1];
array = Append[array, e];
scaledArray = scale array;
pointer = e scale;
];
time += range[[3]];
If[time >= range[[2]], TaskRemove[task]];
];
animate;
(* async task to animate every 50 ms *)
task = SetInterval[animate, 50];
(* stop the task if cell was destroyed or reevaluated *)
EventHandler[cell, {"Destroy"->Function[Null, TaskRemove[task]; Print["removed"]]}];
Graphics[{Red, PointSize[0.05], Point[pointer // Offload],
Opacity[0.5], Line[scaledArray // Offload]
}, TransitionDuration->50, TransitionType->"Linear", Controls->False, PlotRange->{{-1,1}, {-1,1}}]
]
This will sample a given parametric equation and animate it with 50 ms
time step, while on Javascript's side it will interpolate between frames, so that overall animation will look smooth and will be rendered at 60FPS
ParametricAnimator[Exp[Sin[t]] - 2 Cos[4t] + Sin[(2t - Pi)/24], t, {0,16, 0.05}]
Way 3
If you animation depends on some interaction with a user, it might be a good idea to run it and update objects attributes only, when some event is fired.
For example
pt = {0,0};
Graphics[{
White,
EventHandler[
Rectangle[{-2,-2}, {2,2}],
{"mousemove"->Function[xy, pt = xy]}
],
PointSize[0.05], Cyan,
Point[pt // Offload]
}]
a mouse follower
A remark on color and opacity
RGBColor as well as Opacity do support dynamic updates in the context of Graphics. Here it is a bit tricky, since all graphics symbols sharing the same scope should bind to them indirectly. The good news, you do not have to think about and just
color = {1,0,0};
Graphics[{RGBColor[color // Offload], Disk[{0,0}, 1]}]
EventHandler[InputJoystick[], Function[xy,
color = Normalize[{xy[[1]], xy[[2]], 0.5}] // Abs;
]]
Or even more complicated - combining it together with traditional dynamics with nested variables
opacity = 0.5;
Graphics[{Opacity[Offload[opacity]], Red, Disk[{0,0}, Offload[1-opacity]], Blue, Opacity[Offload[1.0 - opacity]], Disk[{0,0}, Offload[opacity]]}, ImagePadding->None]
EventHandler[InputRange[0,1,0.1], Function[value,
opacity = value;
]]
Creating and removing objects
The most examples given on the pages Dynamics, AnimationFrameListener considers only changing the attributes of created graphics primitives on the screen. One can also use pure raster graphics together with Image, however, this is quite cumbersome to deal with.
However, you have seen MetaMarker, you might know the solution. The technique of selecting already evaluated entities on the frontend and evaluating or removing other symbols inside its context comes handy here.
Simple example
Here we will append colorful Disk s to a Graphics symbol context following the mouse position. As usual the best way to do it is to use white Rectangle 😀
Graphics[{White, EventHandler[Rectangle[{-1,-1}, {1,1}], {"mousemove"->handler}], MetaMarker["canvas"]}, ImagePadding->None]
The last thing is to define handler
function
With[{win = CurrentWindow[]},
handler = Function[xy,
FrontSubmit[{
Hue[RandomReal[{0,1}], 1,1],
Disk[xy, RandomReal[{0.01,0.1}]]
}, MetaMarker["canvas"], "Window"->win]
];
];
Here we use sort of selector MetaMarker, which makes sure, that the result will be evaluated in the context of all instances marked as canvas
. The next odd thing is CurrentWindow. We need this otherwise our anonymous function does not know to which window it should send an expression
Animating bubbles
We can go further and not only animate bubbles, but also remove them, when there are to many of them.
The complexity of many instances comes mostly from the fact, that we don't have a direct access from Wolfram Kernel to the frontend, but only WebSockets protocol, which gives a big overhead. The only solution to maintain good performance is to minimize numbers of transactions.
Think about it was a GPU-CPU communication, which also comes with similar restrictions.
Pool of objects
This is going to be our buffers
cPool = Table[{0.,0.}, {i,100}]; (* positions *)
vPool = cPool; (* velocities *)
rPool = Table[0., {i,100}]; (* radius or lifetime *)
oPool = Table[Null, {i,100}]; (* references to objects *)
The general idea is not to allocate new variables for new object, but rather reuse objects from the pool.
Graphical output is going to be the same
Graphics[{White, EventHandler[Rectangle[{-1,-1}, {1,1}], {"mousemove"->handler}], MetaMarker["canvas"]}, ImagePadding->None]
Our future animation loop is going to look like this
With[{win = CurrentWindow[]},
handler = Function[xy,
If[!created[xy, "canvas", win], update[win]];
];
];
We don't need to evaluate it now
An update functions - just go over our arrays and produce new
update[win_] := With[{},
{cPool, rPool} = Transpose[MapIndexed[Function[{a, index},
(* if slot is not empty - recalculate *)
If[oPool[[index//First]] =!= Null,
If[a[[2]] <= 0.002,
(* if radius is too small - remove an object *)
remove[index//First, win];
a
,
(* if ok - animate *)
{a[[1]] + 0.05 vPool[[index//First]], 0.9 a[[2]]}
]
,
a
]
], {cPool, rPool} // Transpose]];
];
if a lifetime is close to zero, we need to remove created instance and free some slots in our buffers for new objects
remove[index_, win_] := (
(* destroy instance on the frontend *)
Delete[oPool[[index]], "Window"->win];
oPool[[index]] = Null
);
And finally a function to create new objects
created[xy_, marker_String, win_] := With[{
(* find empty slot *)
slot = FirstPosition[oPool, Null]
},
If[!MissingQ[slot],
With[{s = slot // First},
(* initial positions and etc *)
cPool[[s]] = xy;
rPool[[s]] = 0.05;
vPool[[s]] = RandomReal[{-1,1}, 2];
oPool[[s]] = True;
(* update so that object wont appear in an odd way *)
update[win];
(* create an instance of Disk on a graph *)
With[{
o = {
Hue[RandomReal[{0,1}],1,1],
(* prevent double updates *)
Disk[Offload[cPool[[s]]], Offload[rPool[[s]], "Static"->True]]
}
},
oPool[[s]] = FrontSubmit[o, MetaMarker[marker], "Window"->win, "Tracking"->True];
];
];
True
,
False
]
]
The big difference to the previous example Simple example is that we track our created instances using an options "Tracking"
, so that we can remove them later for our SVG canvas (aka Graphics)
All positions and radiuses are passed in two solid symbols cPool
and rPool
, then we only need to perform two data transactions to our frontend, which saves a lot of resources, when it comes to make objects flying on the screen. Because of the payload matters less, than each act of transactions in terms of the transport load.